/**
##############################################################################

DOCOMO Innovations, Inc.

Copyright © 2011 DOCOMO Innovations, Inc. All rights reserved.

 

Date of Preparation: 08/01/2011

Contact: kozat@docomoinnovations.com

 

All intellectual property rights related to this software are vested in DOCOMO Innovations.

DOCOMO Innovations is under no obligation to maintain or support the software.

Neither DOCOMO Innovations nor its employees are responsible for any damage or loss resulting from the use of the software.

##############################################################################
 */

package umich.infinity.proxy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.thrift.TException;
import org.apache.thrift.TProcessor;
import org.apache.thrift.TProcessorFactory;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;
import org.apache.thrift.transport.TTransport;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import umich.infinity.facebookconn.Bundle;
import umich.infinity.facebookconn.FacebookConn;
import umich.infinity.facebookconn.FacebookError;
import umich.infinity.facebookconn.FacebookUpdateMonitor;
import umich.infinity.facebookconn.FacebookUploadReq;
import umich.infinity.facebookconn.FacebookUploadScheduler;
import umich.infinity.facebookconn.FacebookUser;
import umich.infinity.facebookconn.FacebookUtil;
import umich.infinity.thrift.Aggregator;
import umich.infinity.thrift.AlbumEntry;
import umich.infinity.thrift.Attr;
import umich.infinity.thrift.DirEntry;
import umich.infinity.thrift.FriendAlbum;
import umich.infinity.thrift.FriendEntry;
import umich.infinity.thrift.MyBinary;
import umich.infinity.thrift.PhotoEntry;
import umich.infinity.thrift.UserToken;

public class AggregatorHandler implements Aggregator.Iface {
	SharedData sharedData;
	
	FacebookUploadScheduler fbUploadScheduler;
	
	FacebookConn facebook;
	
	String appOldId = "193051640748441";
	String appId = "292337034140680";
	
	static final int LOGINREQ = 0;
	static final int LOGOUTREQ = 1;
	
	public class LogInOutReq {
		FacebookUser fbUser;
		int inOut; //LOGINREQ or LOGOUTREQ
	}
	
	public LinkedList<LogInOutReq> logInOutReqs;
	
	public AggregatorHandler() throws FileNotFoundException {
		///print Log
		File test = new File(Para.CacheRoot + "TestingLog.txt");
		PrintStream out = new PrintStream(new FileOutputStream(test));
		System.setOut(out);
		
		sharedData = new SharedData();
		facebook = new FacebookConn(appId);

		logInOutReqs = new LinkedList<LogInOutReq>();
		
		LogInOutUserTask logInOutTask = new LogInOutUserTask();
		Thread thread2 = new Thread(logInOutTask);
		thread2.start();
		
		sharedData.fbUpdateMonitor = new FacebookUpdateMonitor(sharedData);
		Thread thread = new Thread(sharedData.fbUpdateMonitor);
		thread.start();		
		
		fbUploadScheduler = new FacebookUploadScheduler(sharedData);
		Thread uploadThread = new Thread(fbUploadScheduler);
		uploadThread.start();
	}

	/*

	@Override
	public ByteBuffer get_photo(String pid) throws TException {
			System.out.println("get_photo " + pid);
		try {
			String fn = Para.CacheRoot + pid + ".jpg";

			File f = new File(fn);

			if (f.exists()) {
				FileInputStream file = new FileInputStream(f);

				BufferedInputStream in = new BufferedInputStream(file);

				ByteBuffer pData = FacebookUtil.readBin(in);

				return pData;
			}

			String response = facebook.request(pid);
			JSONObject json = FacebookUtil.parseJson(response);
			String sourcePhoto = json.getString("source");

			ByteBuffer pData = FacebookUtil.readBin(FacebookUtil.openUrlBin(sourcePhoto, "GET",
					new Bundle() {
					}));

			System.out.println(pid + " " + sourcePhoto);

			FileOutputStream file = new FileOutputStream(fn);

			BufferedOutputStream out = new BufferedOutputStream(file);
			out.write(pData.array());
			out.flush();
			out.close();

			return pData;
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (FacebookError e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}
		*/

	/*

	@Override
	public ByteBuffer get_video(String path) throws TException {
		try {
			String response = facebook.request("me/videos/uploaded");
			JSONObject json = FacebookUtil.parseJson(response);
			JSONArray data = json.getJSONArray("data");

			for (int i = 0; i < data.length(); i++) {
				JSONObject obj = data.getJSONObject(i);

				String id = obj.getString("id");
				String sourceVideo = obj.getString("source");
				ByteBuffer pData = FacebookUtil.readBin(FacebookUtil.openUrlBin(sourceVideo,
						"GET", new Bundle() {
						}));

				System.out.println("" + i + " " + id + " " + sourceVideo);

				FileOutputStream file = new FileOutputStream(Para.CacheRoot
						+ id + ".mp4");

				BufferedOutputStream out = new BufferedOutputStream(file);
				out.write(pData.array());
				out.flush();

				out.close();

				return pData;
			}
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (FacebookError e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}
	*/

	/*
	@Override
	public List<PhotoEntry> read_album(String aid) throws TException {
		System.out.println("Read_album " + aid);
		List<PhotoEntry> res;
		try {
			System.out.println("Start photo request...");

			String fn = Para.CacheRoot + "album_" + aid;

			File f = new File(fn);

			String responsePhotos = facebook.request(aid + "/photos");
			System.out.println("****\n" + responsePhotos + "***\n");
			JSONObject jsonPhotos = FacebookUtil.parseJson(responsePhotos);
			JSONArray dataPhotos = jsonPhotos.getJSONArray("data");
			res = new ArrayList<PhotoEntry>(dataPhotos.length());
			for (int i2 = 0; i2 < dataPhotos.length(); i2++) {
				JSONObject obj2 = dataPhotos.getJSONObject(i2);

				PhotoEntry pe = new PhotoEntry();
				pe.id = obj2.getString("id");
				String sourcePhoto = obj2.getString("source");
				pe.updatedTime = obj2.getString("updated_time");

				res.add(pe);

			}

			FileOutputStream fos = new FileOutputStream(f);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(res);

			oos.close();

			return res;
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (FacebookError e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}
	*/

	/*
	@Override
	public List<FriendEntry> read_friendlist(String uid) throws TException {
		System.out.println("Read_albumlist " + uid);
		List<FriendEntry> res;
		try {
			String response = facebook.request(uid + "/friends");
			JSONObject json = FacebookUtil.parseJson(response);
			JSONArray data = json.getJSONArray("data");

			res = new ArrayList<FriendEntry>(data.length());
			for (int i = 0; i < data.length(); i++) {
				JSONObject obj = data.getJSONObject(i);

				String id = obj.getString("id");
				String name = obj.getString("name");

				FriendEntry ae = new FriendEntry();
				ae.id = id;
				ae.name = name;

				res.add(ae);
			}
			return res;
		} catch (JSONException e) {
			e.printStackTrace();
		} catch (FacebookError e) {
			e.printStackTrace();
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}*/

	/*
	@Override
	public List<AlbumEntry> read_albumlist(String uid) throws TException {
		System.out.println("Read_albumlist " + uid);
		List<AlbumEntry> res;
		try {
			String response = facebook.request(uid + "/albums");
			JSONObject json = FacebookUtil.parseJson(response);
			JSONArray data = json.getJSONArray("data");

			res = new ArrayList<AlbumEntry>(data.length());
			for (int i = 0; i < data.length(); i++) {
				JSONObject obj = data.getJSONObject(i);

				String id = obj.getString("id");
				String name = obj.getString("name");

				AlbumEntry ae = new AlbumEntry();
				ae.id = id;
				ae.name = name;

				res.add(ae);
			}
			return res;
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (FacebookError e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}*/

	/*
	@Override
	public ByteBuffer get_photo_thumb(String pid) throws TException {
		System.out.println("get_photo " + pid);
		try {
			String response = facebook.request(pid);
			JSONObject json = FacebookUtil.parseJson(response);
			String sourcePhoto = json.getString("picture");

			ByteBuffer pData = FacebookUtil.readBin(FacebookUtil.openUrlBin(sourcePhoto, "GET",
					new Bundle() {
					}));

			System.out.println(pid + " " + sourcePhoto);

			FileOutputStream file = new FileOutputStream(Para.CacheRoot + pid
					+ "_thumb.jpg");

			BufferedOutputStream out = new BufferedOutputStream(file);
			out.write(pData.array());
			out.flush();
			out.close();

			return pData;
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (FacebookError e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}*/

	@Override
	public MyBinary backend_read(UserToken token, String path, long offset,
			long size) throws TException {
		System.out.println(getCurrentMethodName() + ": " + path + " (+"
				+ offset + "," + size + ")");
				
		MyBinary mb = new MyBinary();
		mb.data = localRead(token, path, offset, size);
		
		return mb;
	}

	public String getCurrentMethodName() {
		StackTraceElement stackTraceElements[] = (new Throwable())
				.getStackTrace();
		return stackTraceElements[1].toString();
	}

	@Override
	// If size = 0, read the whole file
	public MyBinary backend_prefetch(UserToken token, String path,
			long offset, long size) throws TException {

		System.out.println(getCurrentMethodName() + ": " + path + " (+"
				+ offset + "," + size + ")");
		
		MyBinary mb = new MyBinary();
		mb.data = localRead(token, path, offset, size);
		
		return mb;
	}
	
	public ByteBuffer localRead(UserToken token, String path,
			long offset, long size) throws TException {

		String fn = Para.CacheRoot + path;
		File f = new File(fn);

		if (!f.exists())
			return null;

		long r_size = size;
		if (size == 0) {
			r_size = f.length();
		} else {
			if (r_size > f.length() - offset) {
				r_size = f.length() - offset;
			}
		}

		byte[] buf = new byte[(int) r_size];
		try {
			RandomAccessFile raf = new RandomAccessFile(f, "r");

			raf.seek(offset);
			raf.read(buf, 0, (int) r_size);
			raf.close();

			return ByteBuffer.wrap(buf);
		} catch (IOException x) {
			x.printStackTrace();
			return null;
		}
	}

	@Override
	public Attr backend_getAttr(UserToken token, String path) throws TException {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<DirEntry> backend_readDir(UserToken token, String path)
			throws TException {
		File dir = new File("/home/stgyd/InfinityProxy");

		File[] list = dir.listFiles();

		List<DirEntry> ret = new ArrayList<DirEntry>();

		for (int i = 0; i < list.length; i++) {
			if (list[i].exists() && list[i].isFile()) {
				DirEntry de = new DirEntry();
				de.attr = new Attr();
				de.name = list[i].getName();
				de.attr.size = list[i].length();
				de.attr.priority = 0;
				ret.add(de);
			}
		}

		return ret;
	}

	public List<DirEntry> backend_pull(UserToken token, long time)
			throws TException {
		//TODO maintain the list notifications in the back-end, and read the list on demand
		
		//Read from the local file system
		String fn = Util.notiName(""+token.uid);
				
		File f = new File(fn);
		if (!f.exists()) {
			return null;
		}
		
		FileInputStream fis;
		try {
			fis = new FileInputStream(f);
			ObjectInputStream ois = new ObjectInputStream (fis);
			
			HashMap<String, DirEntry> res = (HashMap<String, DirEntry>) ois.readObject();
			ois.close();
			f.delete();
			ArrayList<DirEntry> l = new ArrayList<DirEntry>(res.values());
			return l;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		return new ArrayList<DirEntry>();
	}

	public List<DirEntry> backend_push(UserToken token, long time, long waitTime)
			throws TException {
		//Read from the local file system
		String fn = Util.notiName(""+token.uid);
		File f = new File(fn);
		
		long userLoginTime=0;
		
		synchronized (sharedData.fbUsers) {
			userLoginTime = sharedData.fbUsers.get(token.uid).loginTime;
		}
		
		long curTime = System.currentTimeMillis();
		long targetTime = curTime + waitTime;
				
		while ((curTime = (System.currentTimeMillis())) < targetTime) {
			System.out.println("backend_push routine " + token.uid + " " + sharedData.userNotiMonitor.get(token.uid));
			synchronized (sharedData.userNotiMonitor.get(token.uid)) {
				long curUserLoginTime = sharedData.fbUsers.get(token.uid).loginTime;
				
				if (curUserLoginTime != userLoginTime) {
					break;
				}
				
			if (f.exists()) {
				FileInputStream fis;
				try {
					fis = new FileInputStream(f);					
					ObjectInputStream ois = new ObjectInputStream (fis);
					HashMap<String, DirEntry> res = (HashMap<String, DirEntry>) ois.readObject();
					ois.close();
					f.delete();
					ArrayList<DirEntry> l = new ArrayList<DirEntry>(res.values());
					return l;
				} catch (FileNotFoundException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}
			}
			}
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}			
		}
		
		return new ArrayList<DirEntry>();
	}
	
	@Override
	public String backend_new_write(UserToken token, long size) throws TException {
		System.out.println(getCurrentMethodName() + " " + token.uid + " " + size) ;
		long lid = sharedData.cacheManager.getNextCounter(token.uid);
		String ret = "" + lid;
		
		Attr attr = new Attr();
		attr.size = size;
		
		String fn = token.uid + "/" + ret;
		sharedData.cacheManager.initFile(fn, attr);
		
		System.out.println(getCurrentMethodName() + " File name " + fn);
		
		return fn;
	}
	
	@Override
	public long backend_write(UserToken token, String path, ByteBuffer buf,
			long offset, long size) throws TException {
		
		System.out.println(getCurrentMethodName() + ": " + path + " (+"
				+ offset + "," + size + ")");
		
		sharedData.cacheManager.writeFileData(path, offset, size, buf);
		
		Attr attr = sharedData.cacheManager.readFileAttr(path);
		
		if (offset + size >= attr.size) {
			//Add a upload request to the uploading queue
			System.out.println(getCurrentMethodName() + ": add new upload request for file " + path);
			FacebookUploadReq req = new FacebookUploadReq();
			req.uid = token.uid;
			req.localPath = Util.uploadFileData(path);
			
			synchronized (sharedData.uploadQueue) {
				sharedData.uploadQueue.addLast(req);
			}
			
			synchronized (fbUploadScheduler) {
				fbUploadScheduler.notifyAll();
			}			
		}
		
		return size;
	}

	@Override
	public void backend_setAttr(UserToken token, String path, Attr attr)
			throws TException {
		// TODO Auto-generated method stub
		
	}

	public class LogInOutUserTask implements Runnable {
		@Override
		public void run() {
			while (true) {
				LogInOutReq req = null;
				int count = 0;
				
				synchronized (logInOutReqs) {
					if (logInOutReqs.size() > 0) {
						req = logInOutReqs.pop();
					}
				}
				
				while (req != null) {
					count ++;
					synchronized (sharedData.fbUsers) {
						FacebookUser fu =req.fbUser;

						if (req.inOut == LOGINREQ) {
							System.out.println("LogInOutUserTask: " + "Login " + fu.uid);
							if (sharedData.fbUsers.get(fu.uid) != null) {
									sharedData.fbUsers.put(fu.uid, fu);
							} else {
								sharedData.fbUsers.put(fu.uid, fu);
								sharedData.userNotiMonitor.put(fu.uid, 1);								
							}
							System.out.println("LogInOutUserTask: " + "fbUsers.size " + sharedData.fbUsers.size());
						} else {
							System.out.println("LogInOutUserTask: " + "Logout " + fu.uid);
							boolean found = false;
								if (sharedData.fbUsers.get(fu.uid)!= null) {
									sharedData.fbUsers.remove(fu.uid);
									sharedData.userNotiMonitor.remove(fu.uid);
									found = true;									
								}				
									
							System.out.println("LogInOutUserTask: " + "fbUsers.size " + sharedData.fbUsers.size());
						}
					}
					
					synchronized (logInOutReqs) {
						if (logInOutReqs.size() > 0) {
							req = logInOutReqs.pop();
						} else {
							req = null;
						}
					}					
				}
				
				if (count > 0) {
					sharedData.wakeupUpdateScheduler();
				}
				
				synchronized (logInOutReqs) {
					try {
						logInOutReqs.wait(0);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}
	}
	
	@Override
	public void backend_login(String uid, String token) throws TException {
		System.out.println("AggregatorHandler backend_login(String uid, String token)");
		System.out.println("backend_login: " + uid + " " + token);
		FacebookUser fu = new FacebookUser();
		fu.facebook = new FacebookConn(appId);
		fu.token = token;
		fu.facebook.setAccessToken(fu.token, 0);
		fu.uid = uid;
		fu.token = token;
		fu.newUser = true;
		fu.loginTime = System.currentTimeMillis();
	
		LogInOutReq inReq = new LogInOutReq();
		inReq.fbUser = fu;
		inReq.inOut = LOGINREQ;
		
		synchronized (this.logInOutReqs) {
			logInOutReqs.addLast(inReq);
			logInOutReqs.notifyAll();
		}
	}
	
	@Override
	public void backend_logout(String uid, String token) throws TException {
		System.out.println("backend_logout: " + uid + " " + token);
		FacebookUser fu = new FacebookUser();
		fu.facebook = new FacebookConn(appId);
		fu.token = token;
		fu.facebook.setAccessToken(fu.token, 0);
		fu.uid = uid;
		fu.token = token;
		fu.newUser = true;
		
		LogInOutReq outReq = new LogInOutReq();
		outReq.fbUser = fu;
		outReq.inOut = LOGOUTREQ;
		
		synchronized (this.logInOutReqs) {
			logInOutReqs.addLast(outReq);
			logInOutReqs.notifyAll();
		}
	}

	@Override
	public List<DirEntry> backend_resync(List<DirEntry> localList)
			throws TException {
		// TODO Auto-generated method stub
		return null;
	}
		
	@Override
	public int backend_log_upload(UserToken token, String path, ByteBuffer buf,
			long offset, long size) throws TException {
		System.out.println(getCurrentMethodName() + ": " + path + " (+"
				+ offset + "," + size + ")");
		
		sharedData.logManager.write(token.uid, path, offset, size, buf);
		return 0;
	}	
	
	/**
	 * @param args	
	 */
	public static void main(String[] args) {

		// FacebookConn facebook = new FacebookConn("193051640748441");
		// facebook.setAccessToken(token, 0);
		// facebook.action();
		try {
			final AggregatorHandler handler = new AggregatorHandler();

			TProcessorFactory processorFactory = new TProcessorFactory(null) {
				@Override
				public TProcessor getProcessor(TTransport trans) {

					return new Aggregator.Processor(handler);
				}
			};

			// Aggregator.Processor processor = new
			// Aggregator.Processor(handler);
			TServerTransport serverTransport = new TServerSocket(9999);
			// TServer server = new TSimpleServer(processor, serverTransport);

			// Use this for a multithreaded server
			TServer server = new TThreadPoolServer(processorFactory,
					serverTransport);

			System.out.println("Starting the server...");
			server.serve();

		} catch (Exception x) {	
			x.printStackTrace();
		}
		System.out.println("done.");
	}
}
